创建型模式-单例模式
单例模式是什么?
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于 创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
1、单例类 只能有一个 实例。 2、单例类必须 自己创建自己 的唯一实例。 3、单例类必须给 所有其他对象 提供这一实例。
懒汉式单例
- 适用于单线程环境(不推荐)
- 适用于多线程环境,但效率不高(不推荐)
- 双重检验锁
- 静态内部类方式(推荐)
饿汉式单例
- 饿汉式(推荐)
- 枚举方式(推荐)
饿汉式(常用)
是否多线程安全:是
描述:这种方式比较常用,但容易产生垃圾对象。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,但是 instance 在类装载时就实例化 如果没有用到这个实例,那这个内存就浪费了
SingleObject.java
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
枚举方式(推荐)
创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且还能防止反序列化导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。
public class Temp {
public static void main(String[] args) {
Temp single = Temp.getInstance(); // 获取单例
}
private Temp() {}
public static Temp getInstance() {
return Singleton.INSTANCE.getInstance();
}
// 枚举是线程安全的,并且只会装载一次
private enum Singleton {
INSTANCE;
private final Temp instance;
Singleton() {
instance = new Temp();
}
private Temp getInstance() {
return instance;
}
}
}
懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式(线程安全)
使用同步锁 synchronized
防止多线程同时进入造成 instance
被多次实例化。
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,因为实际情况中 99% 情况下不需要同步。(只有第一次创建时需要加锁)
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检验锁
为了在多线程环境下,不影响程序的性能,不让线程每次调用 getInstance()
方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在。
public static Singleton getInstance() {
// 先判断实例是否存在,若不存在再对类对象进行加锁处理
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton1();//error
}
}
}
return instance;
}
下面是上述代码的运行顺序:
- 检测实例是否已经初始化创建,如果是则立即返回
- 否则获得锁
- 再次检测实例是否已经初始化创建成功,如果还没有则创建实例
执行双重检测是因为,如果多个线程通过了第一次检测,并且其中一个首先通过了第二次检测并实例化了对象,剩余的线程不会再重复实例化对象。这样,除了初始化的时候会加锁,后续的调用都是直接返回,解决了多余的性能消耗。
双重检验锁的隐患
但是!!真的这样写是不对的,如下所示,在 IDEA 中抛出了警告
看似天衣无缝,但是这种实现是有隐患的,这个隐患来自于上述代码中注释了 error 的一行,这行代码大致有以下三个步骤:
- 在堆中开辟对象所需空间,分配地址
- 根据类加载的初始化顺序进行初始化
- 将内存地址返回给栈中的引用变量
由于 Java 内存模型允许 “无序写入”,有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了
- 在堆中开辟对象所需空间,分配地址
- 将内存地址返回给栈中的引用变量(此时变量已不在为 null,但是变量却并没有初始化完成)
- 根据类加载的初始化顺序进行初始化
现在考虑重排序后,两个线程出现了如下调用:
此时 T7 时刻 Thread B 对 instance 的访问,访问到的是一个还未完成初始化的对象。所以在使用 instance 时可能会出错。
这时就需要使用到 volatile
来解决内存重排的问题:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton (){}
public static Singleton getInstance() {
// 先判断实例是否存在,若不存在再对类对象进行加锁处理
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类方式
加载一个类时,其静态内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
由于在调用 StaticSingleton.getInstance()
的时候,才会对单例进行初始化,由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。
- 优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
- 劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。
public class StaticSingleton {
private StaticSingleton() {}
public static StaticSingleton getInstance() {
return StaticSingletonHolder.instance;
}
/**
* 一个私有的静态内部类,用于初始化一个静态final实例
*/
private static class StaticSingletonHolder {
private static final StaticSingleton instance = new StaticSingleton();
}
public void methodA() {/* ... */}
public void methodB() {/* ... */}
public static void main(String[] args) {
StaticSingleton.getInstance().methodA();
StaticSingleton.getInstance().methodB();
}
}